GtkSearchEntry: Delay the changed signal by default
authorBastien Nocera <hadess@hadess.net>
Thu, 16 May 2013 16:20:19 +0000 (18:20 +0200)
committerBastien Nocera <hadess@hadess.net>
Fri, 17 May 2013 17:45:46 +0000 (19:45 +0200)
Emit the "changed" signal after 150 msecs, so that searching
through big lists, or doing online searches feels more responsive.

This is something already done in various applications to make
search-as-you type more responsive (gnome-shell, gnome-documents,
gnome-control-center, etc.). The 150 msecs is the value currently
used by gnome-shell, so keep it (invisibly) consistent.

https://bugzilla.gnome.org/show_bug.cgi?id=700229

demos/gtk-demo/Makefile.am
demos/gtk-demo/demo.gresource.xml
demos/gtk-demo/search_entry2.c [new file with mode: 0644]
gtk/gtksearchentry.c

index a5764836cf634a5aaef3a856f55ba138fa923014..8e8da27afe6c0478a8e897dcb40b08e974773250 100644 (file)
@@ -42,6 +42,7 @@ demos =                                               \
        revealer.c                              \
        rotated_text.c                          \
        search_entry.c                          \
+       search_entry2.c                         \
        sizegroup.c                             \
        spinner.c                               \
        stack.c                                 \
index 28c01aadf2736d291affc4159197cbebed345135..b95fcf9304994530ddec4c9468ee6852be9c9ed2 100644 (file)
     <file>revealer.c</file>
     <file>rotated_text.c</file>
     <file>search_entry.c</file>
+    <file>search_entry2.c</file>
     <file>sizegroup.c</file>
     <file>stack.c</file>
     <file>spinner.c</file>
diff --git a/demos/gtk-demo/search_entry2.c b/demos/gtk-demo/search_entry2.c
new file mode 100644 (file)
index 0000000..cdadc78
--- /dev/null
@@ -0,0 +1,96 @@
+/* Entry/Delayed Search Entry
+ *
+ * GtkSearchEntry sets up GtkEntries ready for search. Search entries
+ * have their "changed" signal delayed and should be used
+ * when the searched operation is slow such as loads of entries
+ * to search, or online searches.
+ */
+
+#include <gtk/gtk.h>
+
+static GtkWidget *window = NULL;
+
+static void
+search_entry_destroyed (GtkWidget  *widget)
+{
+  window = NULL;
+}
+
+static void
+search_changed_cb (GtkSearchEntry *entry,
+                   GtkLabel       *result_label)
+{
+  const char *text;
+  text = gtk_entry_get_text (GTK_ENTRY (entry));
+  g_message ("search changed: %s", text);
+  gtk_label_set_text (result_label, text ? text : "");
+}
+
+GtkWidget *
+do_search_entry2 (GtkWidget *do_widget)
+{
+  GtkWidget *content_area;
+  GtkWidget *vbox;
+  GtkWidget *hbox;
+  GtkWidget *label;
+  GtkWidget *entry;
+  GtkWidget *button;
+
+  if (!window)
+    {
+      window = gtk_dialog_new_with_buttons ("Search Entry #2",
+                                            GTK_WINDOW (do_widget),
+                                            0,
+                                            GTK_STOCK_CLOSE,
+                                            GTK_RESPONSE_NONE,
+                                            NULL);
+      gtk_window_set_resizable (GTK_WINDOW (window), FALSE);
+
+      g_signal_connect (window, "response",
+                        G_CALLBACK (gtk_widget_destroy), NULL);
+      g_signal_connect (window, "destroy",
+                        G_CALLBACK (search_entry_destroyed), &window);
+
+      content_area = gtk_dialog_get_content_area (GTK_DIALOG (window));
+
+      vbox = gtk_box_new (GTK_ORIENTATION_VERTICAL, 5);
+      gtk_box_pack_start (GTK_BOX (content_area), vbox, TRUE, TRUE, 0);
+      gtk_container_set_border_width (GTK_CONTAINER (vbox), 5);
+
+      label = gtk_label_new (NULL);
+      gtk_label_set_markup (GTK_LABEL (label), "Search entry demo #2");
+      gtk_box_pack_start (GTK_BOX (vbox), label, FALSE, FALSE, 0);
+
+      /* Create our entry */
+      entry = gtk_search_entry_new ();
+      gtk_box_pack_start (GTK_BOX (vbox), entry, FALSE, FALSE, 0);
+
+      /* Result */
+      hbox = gtk_box_new (GTK_ORIENTATION_HORIZONTAL, 10);
+      gtk_box_pack_start (GTK_BOX (vbox), hbox, TRUE, TRUE, 0);
+      gtk_container_set_border_width (GTK_CONTAINER (hbox), 0);
+
+      label = gtk_label_new ("Result:");
+      gtk_box_pack_start (GTK_BOX (hbox), label, TRUE, TRUE, 0);
+
+      label = gtk_label_new ("");
+      gtk_box_pack_start (GTK_BOX (hbox), label, TRUE, TRUE, 0);
+
+      g_signal_connect (entry, "changed",
+                        G_CALLBACK (search_changed_cb), label);
+
+      /* Give the focus to the close button */
+      button = gtk_dialog_get_widget_for_response (GTK_DIALOG (window), GTK_RESPONSE_NONE);
+      gtk_widget_grab_focus (button);
+    }
+
+  if (!gtk_widget_get_visible (window))
+    gtk_widget_show_all (window);
+  else
+    {
+      gtk_widget_destroy (window);
+      window = NULL;
+    }
+
+  return window;
+}
index b28406a5b4da39de0a1b8ea052ef86c829a8fd1f..b79705b3eee5ad24ca0f90d6af420cbc0e37e2d9 100644 (file)
 
 G_DEFINE_TYPE (GtkSearchEntry, gtk_search_entry, GTK_TYPE_ENTRY)
 
+typedef struct {
+  guint delayed_changed_id;
+  gboolean in_timeout;
+} GtkSearchEntryPrivate;
+
+/* 150 mseconds of delay */
+#define DELAYED_TIMEOUT_ID 150
+
+/* This widget got created without a private structure, meaning
+ * that we cannot now have one without breaking ABI */
+#define GET_PRIV(e) G_TYPE_INSTANCE_GET_PRIVATE (e, GTK_TYPE_SEARCH_ENTRY, GtkSearchEntryPrivate)
+
+static void
+gtk_search_entry_finalize (GObject *object)
+{
+  GtkSearchEntryPrivate *priv = GET_PRIV (object);
+
+  if (priv->delayed_changed_id > 0)
+    g_source_remove (priv->delayed_changed_id);
+
+  G_OBJECT_CLASS (gtk_search_entry_parent_class)->finalize (object);
+}
+
 static void
 gtk_search_entry_class_init (GtkSearchEntryClass *klass)
 {
+  GObjectClass *object_class = G_OBJECT_CLASS (klass);
+
+  object_class->finalize = gtk_search_entry_finalize;
+
+  g_type_class_add_private (klass, sizeof (GtkSearchEntryPrivate));
 }
 
 static void
@@ -63,14 +91,42 @@ search_entry_clear_cb (GtkEntry *entry,
   gtk_entry_set_text (entry, "");
 }
 
+static gboolean
+gtk_search_entry_changed_timeout_cb (gpointer user_data)
+{
+  GtkSearchEntry *entry = user_data;
+  GtkSearchEntryPrivate *priv = GET_PRIV (entry);
+
+  priv->in_timeout = TRUE;
+  g_signal_emit_by_name (entry, "changed");
+  priv->delayed_changed_id = 0;
+  priv->in_timeout = FALSE;
+
+  return G_SOURCE_REMOVE;
+}
+
+static void
+reset_timeout (GtkSearchEntry *entry)
+{
+  GtkSearchEntryPrivate *priv = GET_PRIV (entry);
+
+  if (priv->delayed_changed_id > 0)
+    g_source_remove (priv->delayed_changed_id);
+  priv->delayed_changed_id = g_timeout_add (DELAYED_TIMEOUT_ID,
+                                            gtk_search_entry_changed_timeout_cb,
+                                            entry);
+}
+
 static void
-search_entry_changed_cb (GtkEntry *entry,
-                         gpointer  user_data)
+search_entry_changed_cb (GtkSearchEntry *entry,
+                         gpointer        user_data)
 {
+  GtkSearchEntryPrivate *priv = GET_PRIV (entry);
   const char *str, *icon_name;
   gboolean active;
 
-  str = gtk_entry_get_text (entry);
+  /* Update the icons first */
+  str = gtk_entry_get_text (GTK_ENTRY (entry));
 
   if (str == NULL || *str == '\0')
     {
@@ -91,6 +147,15 @@ search_entry_changed_cb (GtkEntry *entry,
                 "secondary-icon-activatable", active,
                 "secondary-icon-sensitive", active,
                 NULL);
+
+  /* Don't stop the emission if it's the timeout
+   * emitting the signal, otherwise we'll get in a loop */
+  if (priv->in_timeout)
+    return;
+
+  /* Queue up the timeout */
+  reset_timeout (entry);
+  g_signal_stop_emission_by_name (entry, "changed");
 }
 
 static void
@@ -106,8 +171,6 @@ gtk_search_entry_init (GtkSearchEntry *entry)
                 "primary-icon-activatable", FALSE,
                 "primary-icon-sensitive", FALSE,
                 NULL);
-
-  search_entry_changed_cb (GTK_ENTRY (entry), NULL);
 }
 
 /**